package com.eischet.lookup.web;

import org.apache.log4j.Logger;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.Document;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.search.highlight.*;
import org.apache.lucene.util.Version;

import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static com.eischet.lookup.Functional.join;

/**
 * Wraps a Lucene Query.
 */
public class LuceneQuery {

    private static final Logger logger = Logger.getLogger(LuceneQuery.class);

    private final SearchIndex searchIndex;
    private final IndexSearcher searcher;
    private final Analyzer analyzer;
    private final TopDocs hits;
    private final Highlighter highlighter;
    private final int numberOfHits;
    private final String queryString;

    /**
     * @return the number of hits found by the query
     */
    public int getNumberOfHits() {
        return numberOfHits;
    }

    /**
     * Wraps a Query with a recency boosting query.
     *
     * @param useRecencyBoosting        true to use recency boosting, false to not use it
     * @param query                     an unwrapped query
     * @return  a wrapped               query
     */
    private static Query boostRecentUpdates(boolean useRecencyBoosting, Query query) {
        return useRecencyBoosting ? new RecencyBoostingQuery(query, 2.0, 3560, "update") : query;
    }

    /**
     * Create a lucene query instance.
     *
     * @param searchIndex               the index to search
     * @param searcher                  an index searcher to use
     * @param queryString               the query string, as entered by the user
     * @param useRecencyBoosting        true to enable recency boosting, false to disable
     * @param accessController          an access controller that can apply rights filtering to the query before it is executed
     * @param loginName                 the user's login name (required by the access controller)
     * @param session                   the user's HTTP session (required by the access controller)
     * @param filter                    an optional query filter
     * @throws Exception                on some possible error situations
     */
    public LuceneQuery(SearchIndex searchIndex, IndexSearcher searcher, String queryString, boolean useRecencyBoosting, AccessController accessController, String loginName, HttpSession session, Filter filter) throws Exception {
        this.searchIndex = searchIndex;
        this.searcher = searcher;
        this.queryString = queryString;
        analyzer = new StandardAnalyzer(Version.LUCENE_30);
        QueryParser parser = new QueryParser(Version.LUCENE_30, "all", analyzer);
        parser.setDefaultOperator(QueryParser.Operator.AND);
        parser.setAllowLeadingWildcard(true);
        parser.setPhraseSlop(3);
        parser.setDateResolution(DateTools.Resolution.DAY);
        Query query = boostRecentUpdates(useRecencyBoosting, accessController.rewriteQuery(loginName, session, parser.parse(queryString)));
        hits = searcher.search(query, filter, Integer.MAX_VALUE);
        numberOfHits = hits.totalHits;
        Query highlightQuery = parser.parse(queryString).rewrite(searcher.getIndexReader());
        QueryScorer scorer = new QueryScorer(highlightQuery);
        Formatter formatter = new SimpleHTMLFormatter();
        // later: Formatter formatter = new GradientFormatter()
        Encoder encoder = new SimpleHTMLEncoder();
        highlighter = new Highlighter(formatter, encoder, scorer);
        highlighter.setMaxDocCharsToAnalyze(128000);
    }

    /**
     * Hit-highlights a fields contents.
     * @param doc           a Lucene document
     * @param fieldName     the name of a field
     * @return              highlighted HTML
     */
    public String highlight(Document doc, String fieldName) {
        String fieldValue = doc.get(fieldName);
        if (fieldValue != null && !fieldValue.isEmpty()) {
            try {
                String fragment = join(" ... ", Arrays.asList(highlighter.getBestFragments(analyzer, fieldName, fieldValue, 5)));
                if (logger.isDebugEnabled()) {
                    logger.debug(String.format("highlight(fieldName=%s) --> '%s' --> '%s'", fieldName, fieldValue, fragment));
                }
                fieldValue = fragment;
            } catch (IOException e) {
                logger.error(String.format("exception highlighting fragment for fieldName=%s in doc=%s", fieldName, doc), e);
            } catch (InvalidTokenOffsetsException e) {
                logger.error(String.format("exception highlighting fragment for fieldName=%s in doc=%s", fieldName, doc), e);
            } catch (BooleanQuery.TooManyClauses e) {
                logger.warn("too many clauses exception highlighting fragment with query '"+queryString+"'");
            }
        }
        return fieldValue;
    }

    /**
     * Return query hits.
     *
     * @param startIndex        hit number to start from
     * @param pageSize          number of hits to load
     * @return                  a list of hits
     */
    List<SearchResult> getHits(int startIndex, int pageSize) {
        if (logger.isDebugEnabled()) {
            logger.debug(String.format("getHits(startIndex=%s, pageSize=%s), numberOfHits=%s", startIndex, pageSize, numberOfHits));
        }
        try {
            int endIndex = startIndex + pageSize;
            if (numberOfHits < 1 || numberOfHits < startIndex) {
                return Collections.emptyList();
            }
            endIndex = Math.min(endIndex, numberOfHits);
            ScoreDoc[] scoreDocs = hits.scoreDocs;
            List<SearchResult> docs = new ArrayList<SearchResult>(pageSize);
            for (int i=startIndex; i<endIndex; ++i) {
                Document doc = searcher.doc(scoreDocs[i].doc);
                docs.add(new SearchResult(searchIndex, doc, this));
            }
            return docs;
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * @return a String representation of this object
     */
    @Override
    public String toString() {
        return String.format("LuceneQuery(numberOfHits=%d, queryString='%s')", numberOfHits, queryString);
    }

    void close() {
        this.searchIndex.close(this.searcher);
    }

}
